(** Bombs-Must-Detonate: FSM Syntax Extension Compiler
    @author Brian Go *)

open Ast
exception Fsm_error of string


let raise_fsm_error strlist = raise (Fsm_error (String.concat "" strlist))
let concat s = String.concat "" s

(** A node in the state graph used for connectivity checking *)
type state_node = int * int list * bool ref (** id, target id list, visited flag *)

(** State Graph *)
let node_list = ref []

(** State Id -> State name association *)
let state_env = ref []

(** Returns the state name given an id *)
let get_state_name id = 
  if List.mem_assoc id !state_env then 
    List.assoc id !state_env
  else
    string_of_int id

(** Reverses key/value pairs of an association list *)
let rec reverse_assoc lst = match lst with
    (x,y)::rest -> (y,x)::(reverse_assoc rest)
  | [] -> []

(** Retrieves the state_node object with the given id from the given graph *)
let rec get_state_helper node_list search_id = match node_list with
    (id,targets,visited)::rest when id = search_id -> (id,targets,visited)
  | _::rest -> get_state_helper rest search_id
  | [] -> raise_fsm_error ["State machine error: state ";
			   get_state_name search_id;
			   " has an incoming transition but is not defined."]

(** Retrieves the state_node object with the given id from the globally defined graph *)
let get_state search_id = get_state_helper !node_list search_id

(** Visit all nodes stemming from nodes in node_list *)
let rec visit_graph node_list = 
  match node_list with
      (id,targets,visited)::rest ->
	if !visited then
	  visit_graph rest
	else 
	  let _ = visited := true in
	  let _ = visit_graph (List.map get_state targets) in
	    visit_graph rest
    | [] -> ()


(** Check if the graph node_list with root(s) start_nodes is connected *)
let check_connected start_nodes node_list =
  let _ = visit_graph start_nodes in
  let rec helper node_list = match node_list with
      (id,_,visited)::rest -> 
	if !visited then 
	  helper rest 
	else 
	  raise_fsm_error ["State machine error: state graph is disconnected.  State ";
			   get_state_name id;
			   " is not reachable."]
    | [] -> ()
  in 
    helper node_list


(** Used by make_id() *)
let cur_id = ref (-1);;
(** Returns a unique integer every call *)
let make_id () = let _ = cur_id := !cur_id + 1 in !cur_id

(** Returns the first element of any 2-tuple *)
let rec get_first lst = match lst with
    (x,y)::rest -> x::(get_first rest)
  | [] -> []

(** Gets a string list of state names from a list of state_nodes *)
let rec get_state_names statelist = match statelist with
    (name,_,_)::rest ->
      name::(get_state_names rest)
  | [] -> []

(** Appends the first string to the second *)
let append_string app_str str = 
  concat [str;app_str]

(** Checks if the id is associated with some name in the environment.
    If it is, returns the name, and then unchanged environment.
    If it isn't, creates a new ID, adds it to the environment, and returns them *)
let get_id name env = 
  if List.mem_assoc name env then
    List.assoc name env, env
  else
    let id = make_id() in
      id, (name,id)::env

(** Turns a string into a AST variable identifier *)
let varify str = SynVarIdentifier (SynVarName str)

(** Compiles the callback list of a state *)
let rec compile_callbacks callbacks state_name = 
  match callbacks with 
      cur::rest ->
	let field, foo = 
	  (match cur with
	       SynFsmOnInit s -> "onInit", s (** Not actually a callback.  Called when the state is viisted. *)
	     | SynFsmOnMoveRequest s -> "onMoveRequest", s
	     | SynFsmOnTeammateDeath s -> "onTeammateDeath", s
	     | SynFsmOnBombDetonate s -> "onBombDetonate", s
	     | SynFsmOnDeath s -> "onDeath", s)
	in
	  (if field = "onInit" then (** Process onInit differently *)
	     [ SynVarAssign
		 (SynVarAssignment
		    (SynStructOrEnumValue
		       (SynVarName state_name,
			"init",
			ref VIdUnspecified),
		     SynVarIdentifier
		       (SynVarName foo))) ]
	   else
	     [ SynVarAssign
		 (SynVarAssignment
		    ((SynStructOrEnumValue
			(SynVarName (concat [state_name;"_callbacks"]),
			 field, ref VIdUnspecified)),
		     (SynVarIdentifier
			(SynVarName foo)))) ])
	  @ compile_callbacks rest state_name
    | [] -> []

(** Compile a transition target of a state.  A transition target is a state name with 
    a weight.  If there are multiple targets then the individual weight divided by the
    total weight is the probability that target state is visited next. *)
let compile_transition_target (targ_name,targ_weight) transition_name env = 
  let targ_id, new_env = 
    if List.mem_assoc targ_name env then
      List.assoc targ_name env, env
    else 
      let id = make_id() in
	id, (targ_name,id)::env
  in
  let tuple_name = concat [transition_name;"_target_";string_of_int (make_id())] in
    [ (** declare transition target tuple *)
      SynVarDeclare
	(SynVarDeclareNoInit
	   (SynEnumOrStructType
	      ("fsmTransitionTarget", ref []),
	    tuple_name));
      (** set target id *)
      SynVarAssign
	(SynVarAssignment
	   (SynStructOrEnumValue
	      (SynVarName tuple_name,
	       "id",
	       ref VIdUnspecified),
	    SynValue
	      (SynIntValue targ_id)));
      (** set the target weight *)
      SynVarAssign
	(SynVarAssignment
	   (SynStructOrEnumValue
	      (SynVarName tuple_name,
	       "weight",
	       ref VIdUnspecified),
	    SynValue
	      (SynFloatValue targ_weight)));
      (** add tuple to transition list *)
      SynVarAssign
	(SynVarModify
	   (SynStructOrEnumValue
	      (SynVarName transition_name,
	       "targets",
	       ref VIdUnspecified),
	    SynBinopCons,
	    SynVarIdentifier
	      (SynVarName tuple_name))) ], new_env, targ_id
	   

(** Compiles all transition targets of a given state.  See compile_transition_target *)
let rec compile_transition_targets targs transition_name env = match targs with
    cur::rest -> 
      let cur_result, cur_env, targ_id = compile_transition_target cur transition_name env in 
      let rest_result, rest_env, targ_ids = compile_transition_targets rest transition_name cur_env in
	cur_result @ rest_result, rest_env, targ_id::targ_ids
  | [] -> [], env, []
   

(** Compiles a given transition in a state.  A transition is a list of predicates which
    act on the fsmStateInformation object (see lib_fsm.bmd), all of which much be satisfied
    to visit (non-deterministaically) one of the transition targets. *)
let compile_transition (preds,targs) state_name env = 
  let name = concat [state_name;"_transition_";string_of_int (make_id())] in
  let predvars = List.map varify preds in
  let target_result, target_env, target_ids = compile_transition_targets targs name env in
    [ (** declare transition *)
      SynVarDeclare
	(SynVarDeclareNoInit
	   (SynEnumOrStructType ("fsmTransition",ref []),
	    name));
      (** assign predicate list *)
      SynVarAssign
	(SynVarAssignment
	   (SynStructOrEnumValue
	      (SynVarName name,
	       "predicates",
	       ref VIdUnspecified),
	    (SynListValueProducer
	       (SynListList predvars))))]
      (** compile target list *)
    @ target_result
      (** add to transition list *)
    @ [ SynVarAssign
	  (SynVarModify
	     (SynStructOrEnumValue
		(SynVarName state_name,
		 "transitionList",
		 ref VIdUnspecified),
	      SynBinopCons,
	      (SynVarIdentifier
		 (SynVarName name)))) ], target_env, target_ids


(** Compiles all transitions of a given state.  See compile_transition *)      
let rec compile_transitions transitions state_name env = 
  match transitions with
      cur::rest -> 
	let cur_result, cur_env, targ_ids = compile_transition cur state_name env in
	let rest_result, rest_env, id_list = compile_transitions rest state_name cur_env in
	  cur_result @ rest_result, rest_env, targ_ids @ id_list
    | [] -> [], env, []


(** Compiles all states.  A state has an initialization function called every time it is visited,
    a set of game event callback functions, and transitions to other states.  The mechanism of 
    state transition is described in lib_fsm.bmd. *)
let rec compile_states_helper state_list env = match state_list with
    (name, callback_list, transition_list)::rest ->
      let id,new_env = get_id name env in
      let transition_code, transition_env, transition_ids = compile_transitions transition_list name new_env in
      let _ = node_list := (id,transition_ids,ref false)::!node_list in
	[ (** declare state *)
	  SynVarDeclare
	    (SynVarDeclareNoInit
	       (SynEnumOrStructType ("fsmState",ref []),
		name));
	  (** assign id *)
	  SynVarAssign 
	    (SynVarAssignment
	       (SynStructOrEnumValue
		  (SynVarName name,"id",ref VIdUnspecified),
		SynValue
		  (SynIntValue id)));
	  (** declare callback set *)
	  SynVarDeclare
	    (SynVarDeclareNoInit
	       (SynEnumOrStructType ("callbackSet",ref []),
		concat [name;"_callbacks"]))]
	@ (** compile callbacks *)
	  compile_callbacks callback_list name
	@ [ (** assign callback set *)
	  SynVarAssign
	    (SynVarAssignment
	       (SynStructOrEnumValue
		  (SynVarName name,"callbacks",ref VIdUnspecified),
		SynVarIdentifier
		  (SynVarName (concat [name;"_callbacks"])))) ]
	@ transition_code
	@ compile_states_helper rest transition_env
  | [] -> let _ = state_env := reverse_assoc env in []

(** Produces code to set the state list of the finite state machine to the given state names *)	   
let compile_set_statelist state_names fsm_name =
  let state_name_vars = List.map varify state_names in
    [ SynVarAssign
	(SynVarAssignment
	   (SynStructOrEnumValue
	      (SynVarName "ret",
	       "states",
	       ref VIdUnspecified),
	    SynListValueProducer
	      (SynListList state_name_vars))) ]

(** Compiles the state list and sets the initial state of the FSM.  See compile_states_helper. *)
let compile_states state_list fsm_name =
  (** Compile all states *) 
  let states = compile_states_helper state_list [] in
  match state_list with 
      (init_name,_,_)::rest ->
	states
	  (** Set the initial state *)
	@ [SynVarAssign
	     (SynVarAssignment
		(SynStructOrEnumValue
		   (SynVarName "ret","currentState",ref VIdUnspecified),
		 SynVarIdentifier
		   (SynVarName init_name)))]
	  (** Make the state list and set the fsm field *)
	@ (compile_set_statelist (get_state_names state_list) fsm_name)
    | [] -> raise_fsm_error ["State machine error: No states specified for state machine."]


(** Compiles each state machine described in the AST, leaving other syntax elements untouched. *)
let rec compile_fsm src_prog = 
    match src_prog with
	(SynStateMachine (name,init,state_list))::rest ->
	  let _ = node_list := [] in
	  let machine_code = 
	    (** Declare fsm, set the init function *)
	    [SynVarDeclare 
	       (SynVarDeclareNoInit 
	      (SynEnumOrStructType ("finiteStateMachine",ref []),
	       "ret"));
	     SynVarAssign
	       (SynVarAssignment 
		  (SynStructOrEnumValue 
		     (SynVarName "ret","initialize",ref VIdUnspecified),
		   SynVarIdentifier
		     (SynVarName init)))]
	      (** Compile the states *)
	    @ compile_states state_list name
	      (** Return the state machine *)
	    @ [SynReturnStatement 
		 (SynValueReturn 
		    (SynVarIdentifier
		       (SynVarName "ret")))]
	  in
	  let machine_function = (SynFunctionDefine ((SynEnumOrStructType ("finiteStateMachine",ref []), name, []), machine_code)) in
	  let start_node = match List.rev !node_list with
	      start::rest -> start
	    | [] -> raise_fsm_error ["State mahine error: no states specified."]
	  in
	  let _ = check_connected [start_node] !node_list in
	    (SynInclude (ref (IncludeFileName "lib_fsm.bmd")))::(machine_function::(compile_fsm rest))
      | cur::rest -> cur::(compile_fsm rest)
      | [] -> []
